#    Rubatom
#    Copyright (c) 2014 Sebastian Dufner
#
#    This file is part of Rubatom.
#
#    Rubatom is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    Rubatom is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.

require 'zlib'

require 'rubatom/block'
require 'rubatom/controller'
require 'rubatom/group'

##
#

module Rubatom

  ##
  # Provides support for StarMade structures.

  class Structure
    include Enumerable

    class SubStructure < Structure
      def initialize(blocks, descriptor)
        @blocks, @descriptor = blocks, descriptor
      end
    end

    class Accessor < Structure
      def initialize(blocks, descriptor)
        @blocks, @descriptor = blocks, descriptor
      end
    end

    def self.read(file)
      struct = Structure.new File.basename(file)

      struct.send :readHeader, file

      Dir.glob("#{file}#{File::Separator}DATA#{File::Separator}*.smd2").each do
          |file|
        struct.send :readShip, file
      end

      struct
    end

    def initialize(name)
      @blocks = []
    end

    ##
    # The Structure's name.

    attr_reader :name
    attr_writer :name

    ##
    # Returns two Arrays with three elements each, describing the
    # Structure's bounding box.

    def bounding_box
      return *@bounding_box
    end

    def dimensions
      lower, upper = bounding_box
      return *lower.zip(upper).map {|l, u| (u - l).to_i }
    end

    def length
      [*dimensions][2] - 2
    end

    def height
      [*dimensions][1] - 2
    end

    def width
      [*dimensions][0] - 2
    end

    def [](index, length = 1)
      case index
      when Range  then  Accessor.new(@blocks, [index])
      else              Accessor.new(@blocks, [[index, length]])
      end
    end

    def []=(*args)
      fail NotImplementedError
    end

    def each
      if block_given?
        @blocks.each {|plane| plane.each {|row| row.each {|block| yield block}}}
      end
    end

    def mass
      count {|b| b} / 10.0
    end

    def projection(dir = :y, &block)
      return projection(dir) {|a, b| a || b } unless block

      case dir
      when :y
        @blocks.map do |plane|
          plane.inject do |proj, row|
            proj.zip(row).map {|pair| block[*pair]}
          end
        end
      when :x
        result = @blocks.transpose.map do |plane|
          plane.inject do |proj, row|
            proj.zip(row).map {|pair| block[*pair]}
          end
        end

        result
      when :z
        @blocks.map do |plane|
          plane.map do |row|
            row.inject &block
          end
        end
      end
    end

    def size
      return @blocks.size, @blocks.first.size, @blocks.first.size
    end

  protected

    attr_reader :blocks
    attr_writer :blocks

  private

    def readHeader(file)
      filename = "#{file}#{File::Separator}header.smbph"
      $logger.info "Reading header info from file '#{filename}'" if $logger

      File.open(filename) do |f|
        # http://www.starmadewiki.com/wiki/File_format#Header_Files_.28.smbph.29
        # Discard 2 integers
        f.read_i; f.read_i

        # Read lower bounding box coordinates
        x_lower, y_lower, z_lower = f.read_f32, f.read_f32, f.read_f32

        # Read upper bounding box coordinates
        x_upper, y_upper, z_upper = f.read_f32, f.read_f32, f.read_f32

        @bounding_box =
            [[x_lower, y_lower, z_lower], [x_upper, y_upper, z_upper]]
        $logger.debug "Bounding box: #{@bounding_box}" if $logger

        # Ship dimensions
        width = x_upper - x_lower
        height = y_upper - y_lower
        length = z_upper - z_lower

        # Array dimensions
        ary_width = (width / 16).ceil * 16
        ary_height = (height / 16).ceil * 16
        ary_length = (length / 16).ceil * 16
        $logger.debug "Chosing array size " +
            "#{ary_width}, #{ary_height}, #{ary_length}" if $logger


        @blocks = Array.new(ary_width) do
          Array.new(ary_height) do
            Array.new(ary_length)
          end
        end

        $logger.debug "Skipping the rest of the file." if $logger
        # Block table length
        # table_size = f.read_i
        # We probably are finished here.
      end

      nil
    end

    def readShip(file)
      $logger.info "Reading ship data from file '#{file}'"
      File.open(file) do |f|
        # http://www.starmadewiki.com/wiki/File_format#Ship_Files_.28.smd2.29
        file_size = f.size
        $logger.debug "file size: #{file_size}" if $logger

        # Read file format.
        file_format = f.read_i32
        $logger.debug "file format: #{file_format}" if $logger

        # Raise an error if we encounter an unknown file format
        unless (0..1).include? file_format
          raise IOError, "unknown file format: #{file_format}"
        end

        # Set some values that we need later
        initial_offset = 65540
        chunk_size = 5120

        active_index = 20
        active_mask = 1
        hp_index = 11
        hp_mask = 0xff
        id_index = 0
        id_mask = 0x7ff
        orient_index = 21
        orient_mask = 0x7

        # Change the above values according to the file format
        if file_format == 1
          initial_offset = 65541
          active_index = 19
          hp_mask = 0x7f
          orient_index = 20
          orient_mask = 0xf
        end

        # Read chunks.
        (initial_offset...file_size).step(chunk_size) do |offset|
          f.seek offset

          # Read timestamp
          timestamp = f.read_i64
          #~ timestamp = 0

          # Read chunk position
          c_x, c_y, c_z = f.read_i32, f.read_i32, f.read_i32

          # Read chunk type
          chunk_type = f.readbyte

          # Read data length
          length = f.read_i32

          $logger.debug do
            "Chunk: offset #{offset}; timestamp #{timestamp}; position "
                .concat "#{c_x},#{c_y},#{c_z}; type #{chunk_type}; length "
                .concat "#{length}"
          end if $logger

          # Inflate chunk data
          inf = Zlib::Inflate.inflate(f.read(length))

          # We need some variables to do the following stuff right
          offset = 0
          org_x, org_y, org_z = *bounding_box[0].map {|x| (x + 8).to_i}

          # Process block data
          16.times do |b_z|
            16.times do |b_y|
              16.times do |b_x|
                data = "\x00#{inf[offset, 3]}".unpack('i>').first
                offset += 3

                # Determine the block's attributes
                id = (data >> id_index) & id_mask
                active = ((data >> active_index) & active_mask) == 1
                hp = (data >> hp_index) & hp_mask
                orientation = (data >> orient_index) & orient_mask

                # Determine the position of the current block in @blocks
                x = b_x + c_x - org_x
                y = b_y + c_y - org_y
                z = b_z + c_z - org_z

                # Fail if we would put a block outside of our Structure's
                # dimensions.
                # XXX: Remove when thoroughly tested.
                fail "#{x},#{y},#{z}" if (@blocks.size <= x || @blocks[x].size <= y || @blocks[x][y].size <= z) && id != 0

                @blocks[x][y][z] = Block.new(
                      id, hp: hp, orientation: orientation,
                      active: active) unless id == 0
              end
            end
          end
        end
      end

      nil
    end

    def readLogic(file)
      File.open("#{file}#{File::Separator}header.smbph") do |f|
        # Discard integer
        f.read_i32

        # Read number of controllers
        num_controllers = f.read_i32

        # Process data
        num_controllers.times do
          # Read controller position
          x_pos, y_pos, z_pos = f.read_i16, f.read_i16, f.read_i16

          # Read group count
          num_groups = f.read_i32

          # Process groups
          num_groups.times do
            #
          end
        end
      end

      nil
    end
  end
end
